import requests              # HTTP请求
import execjs                # JS加密调用
import time                  # 时间戳
from bs4 import BeautifulSoup  # 网页解析
import re                    # 正则处理
import json                  # JSON
import io                    # 图片字节流解包
from PIL import Image        # 图片
import numpy as np           # 数值处理（滑块像素算法）
import random                # 滑轨轨迹
import ddddocr



def get_shuffle_indices():
    e = [int(x) for x in "6_11_7_10_4_12_3_1_0_5_2_9_8".split('_')]
    t = []
    for r in range(52):
        n = 2 * e[int((r % 26) / 2)] + (r % 2)
        if (int(r / 2) % 2) != 0:
            n += -1 if (r % 2) else 1
        if r < 26:
            n += 26
        t.append(n)
    return t

def restore_bg(shuffled_img_bytes, restored_img_path="restored_bg.jpg"):
    # 按你的乱序还原写法
    from PIL import Image
    import io
    im = Image.open(io.BytesIO(shuffled_img_bytes))
    w, h = im.size
    slice_w, slice_h = 12, 80
    row_cnt = 2
    col_cnt = 26

    def get_shuffle_indices():
        e = [int(x) for x in "6_11_7_10_4_12_3_1_0_5_2_9_8".split('_')]
        t = []
        for r in range(52):
            n = 2 * e[int((r % 26) / 2)] + (r % 2)
            if (int(r / 2) % 2) != 0:
                n += -1 if (r % 2) else 1
            if r < 26:
                n += 26
            t.append(n)
        return t

    t = get_shuffle_indices()

    blocks = []
    for idx in range(52):
        row = idx // 26
        col = idx % 26
        left = col * slice_w
        top = row * slice_h
        crop = im.crop((left, top, left + slice_w, top + slice_h))
        blocks.append(crop)

    new_im = Image.new('RGB', (w, h))
    for idx, block_idx in enumerate(t):
        row = idx // 26
        col = idx % 26
        left = col * slice_w
        top = row * slice_h
        new_im.paste(blocks[block_idx], (left, top))

    new_im.save(restored_img_path)
    print(f"还原后已保存: {restored_img_path}")
    return new_im




with open("enc.js", "r", encoding="utf-8") as f:
    js_code = f.read()
ctx = execjs.compile(js_code)

def locate_gap_by_diff(fullbg_img, restore_bg_img):
    # 转灰度
    img1 = np.array(fullbg_img.convert('L'))
    img2 = np.array(restore_bg_img.convert('L'))
    h, w = img1.shape
    min_score = 1e10
    target_x = 0
    gap_width = 54  # 基于实际slice宽度做小窗口搜索
    for x in range(w - gap_width):
        diff = np.sum(np.abs(img1[:, x:x+gap_width] - img2[:, x:x+gap_width]))
        if diff < min_score:
            min_score = diff
            target_x = x
    print("自动diff定位缺口x坐标:", target_x)
    return target_x
def parse_validata_response(response_text):
    """
    解析 validata 响应，自动识别无感或滑块结果并统一返回结构化信息
    :param response_text: jsonp格式
    :return: dict 结构
    """
    # 1. 提取json包裹体（cb(...))）
    match = re.search(r'cb\((\{.*\})\)', response_text, re.DOTALL)
    if not match:
        raise ValueError("找不到有效的JSONP内容")
    data = json.loads(match.group(1))
    # 2. ReturnCode判断
    if data.get('ReturnCode') == '5':
        return {
            'type': 'noclick',
            'code': 5,
            'msg': data.get('Msg', ''),
            'data': None
        }
    elif data.get('ReturnCode') == '0':
        # Data.Result 又是JSON字符串
        raw_result = data.get('Data', {}).get('Result', '')
        result = {}
        if raw_result:
            try:
                result = json.loads(raw_result)
            except Exception as e:
                result = {"error": "结果解析失败:" + str(e), "raw": raw_result}

        return {
            'type': 'slider',
            'code': 0,
            'msg': data.get('Msg', ''),
            'success': result.get('success'),
            'validate': result.get('validate'),
            'score': result.get('score'),
            'raw': result
        }
    else:
        return {
            'type': 'unknown',
            'code': data.get('ReturnCode'),
            'msg': data.get('Msg', ''),
            'raw': data
        }
def login_api():
    headers = {
        "Accept": "application/json, text/javascript, */*; q=0.01",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,ar;q=0.7",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        "Origin": "https://exaccount2.eastmoney.com",
        "Pragma": "no-cache",
        "Referer": "https://exaccount2.eastmoney.com/home/Login4?rc=1607457986",
        "RequestVerificationToken": "X25VqYs5DoRsARzY7J1O1nMU5xK217SQEiNvtk872axvd8PYcJv0t1zftrak-TCe431sNHDL3jX0lyZGrmLfmdQKTwk1",
        "Sec-Fetch-Dest": "empty",
        "Sec-Fetch-Mode": "cors",
        "Sec-Fetch-Site": "same-origin",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0",
        "X-Requested-With": "XMLHttpRequest",
        "deviceType": "Web",
        "domainName": "passport2.eastmoney.com",
        "productType": "UserPassport",
        "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"Windows\""
    }
    cookies = {
        "qgqp_b_id": "0e62c6084addcd8b73f504ed7097ee3a",
        "websitepoptg_api_time": "1754106536586",
        "st_nvi": "uPvtl1P_7OmOxRkvT4AId7e10",
        "nid": "0ba7bcebb6c437b69474411fda4a96e1",
        "nid_create_time": "1754106537299",
        "gvi": "GxgX_Rp8kWc5suJsQPKMH392b",
        "gvi_create_time": "1754106537299",
        "nid_id": "295676914",
        "st_si": "71678253589540",
        "st_asi": "delete",
        "fullscreengg": "1",
        "fullscreengg2": "1",
        "RequestData": "%7b%22agentPageUrl%22%3a%22https%3a%2f%2fpassport2.eastmoney.com%2fpub%2fLoginAgent%22%2c%22redirectUrl%22%3a%22https%3a%2f%2fwww.eastmoney.com%2f%22%2c%22callBack%22%3a%22LoginCallBack%22%2c%22redirectFunc%22%3a%22PageRedirect%22%2c%22data%22%3a%7b%22domainName%22%3a%22passport2.eastmoney.com%22%2c%22deviceType%22%3a%22Web%22%2c%22productType%22%3a%22UserPassport%22%2c%22version%22%3a%220.0.1%22%7d%2c%22type%22%3anull%7d",
        "__RequestVerificationToken": "-5yJY1j_Dwhwa-HPfbeeHoUKKGZatLfYkSSxw0DUNcdJdYALChwW2a9l3dW0cMUmtvLect4l8E0-bfzw-8swfVBXwCU1",
        "p_origin": "https%3A%2F%2Fpassport2.eastmoney.com",
        "_qct": "e805ea4aa36a412d9b6871ae5a46f84a",
        "_qcu": "163.125.168.209abe6e10e",
        "st_pvi": "59157079925959",
        "st_sp": "2025-08-02%2011%3A48%3A57",
        "st_inirUrl": "https%3A%2F%2Fpassport2.eastmoney.com%2F",
        "st_sn": "2",
        "st_psi": "20250802175428944-0-5141008185"
    }
    url = "https://exaccount2.eastmoney.com/JsonAPI/Login3"
    data = {
        "username": "18320975328",
        "password": "ILuql7J6YmRqd6XqZKiy3Jbo/0cyDw2P4yxISd+htel9FVnt+Ge9nmIFNbenHremxz6mvpcUEThlmOVZz59icdYOaN07dKVk7wIm84ZoOP/XH/0FWD4lZ9HEZjiVxRYEIK0YsSBzW5WPrIka36ejX6zAc9b3QzaDjNK1AlFsJIY=",
        "captconetxt": "93ee681c7403a6a205aa43bb8873c3d5",
        "captvalidate": "dbeabbb0568496d49a28f99098137545"
    }
    response = requests.post(url, headers=headers, cookies=cookies, data=data)

    print(response.text)
    print(response)

def validata_request(ctx, ctxid, mode, d, t):
    """
    :param ctx: execjs上下文
    :param ctxid: 当前验证码上下文id
    :param mode: "slide"有感滑块  "init"无感
    :param d: 轨迹字符串
    :param t: 总时长（毫秒）
    """
    req = ctx.call("get_validate", ctxid, mode, d, t)
    headers = {
        "Accept": "*/*",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,ar;q=0.7",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "Pragma": "no-cache",
        "Referer": "https://exaccount2.eastmoney.com/",
        "Sec-Fetch-Dest": "script",
        "Sec-Fetch-Mode": "no-cors",
        "Sec-Fetch-Site": "same-site",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0",
        "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"Windows\""
    }
    cookies = {
        "qgqp_b_id": "0e62c6084addcd8b73f504ed7097ee3a",
        "websitepoptg_api_time": "1754106536586",
        "st_nvi": "uPvtl1P_7OmOxRkvT4AId7e10",
        "nid": "0ba7bcebb6c437b69474411fda4a96e1",
        "nid_create_time": "1754106537299",
        "gvi": "GxgX_Rp8kWc5suJsQPKMH392b",
        "gvi_create_time": "1754106537299",
        "st_si": "71678253589540",
        "st_asi": "delete",
        "fullscreengg": "1",
        "fullscreengg2": "1",
        "p_origin": "https%3A%2F%2Fpassport2.eastmoney.com",
        "st_pvi": "59157079925959",
        "st_sp": "2025-08-02%2011%3A48%3A57",
        "st_inirUrl": "https%3A%2F%2Fpassport2.eastmoney.com%2F",
        "st_sn": "5",
        "st_psi": "20250802190712565-0-3336068507"
    }
    url = "https://smartvcode2.eastmoney.com/Titan/api/captcha/Validate"
    params = {
        "callback": "cb",
        "ctxid": ctxid,
        "request": req,
        "_": str(int(time.time() * 1000))
    }
    response = requests.get(url, headers=headers, cookies=cookies, params=params)
    print("[验证码接口返回]：", response.text)
    return parse_validata_response(response.text)
def locate_gap(bg_img, slice_img):
    bg_arr = np.array(bg_img.convert('L'))
    sl_arr = np.array(slice_img.convert('L'))
    sh, sw = sl_arr.shape
    _, w = bg_arr.shape

    min_offset = -1
    min_diff = float('inf')
    for offset in range(w - sw):
        roi = bg_arr[0:sh, offset:offset+sw]   # 高度只取slice的
        diff = np.sum(np.abs(roi - sl_arr))
        if diff < min_diff:
            min_diff = diff
            min_offset = offset
    return min_offset
def get_bg(ctxid):

    req= ctx.call("get_req", ctxid)
    print(req)
    headers = {
        "Accept": "*/*",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,ar;q=0.7",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "Pragma": "no-cache",
        "Referer": "https://exaccount2.eastmoney.com/",
        "Sec-Fetch-Dest": "script",
        "Sec-Fetch-Mode": "no-cors",
        "Sec-Fetch-Site": "same-site",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0",
        "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"Windows\""
    }
    cookies = {
        "qgqp_b_id": "0e62c6084addcd8b73f504ed7097ee3a",
        "websitepoptg_api_time": "1754106536586",
        "st_nvi": "uPvtl1P_7OmOxRkvT4AId7e10",
        "nid": "0ba7bcebb6c437b69474411fda4a96e1",
        "nid_create_time": "1754106537299",
        "gvi": "GxgX_Rp8kWc5suJsQPKMH392b",
        "gvi_create_time": "1754106537299",
        "st_si": "71678253589540",
        "st_asi": "delete",
        "fullscreengg": "1",
        "fullscreengg2": "1",
        "p_origin": "https%3A%2F%2Fpassport2.eastmoney.com",
        "st_pvi": "59157079925959",
        "st_sp": "2025-08-02%2011%3A48%3A57",
        "st_inirUrl": "https%3A%2F%2Fpassport2.eastmoney.com%2F",
        "st_sn": "7",
        "st_psi": "20250803144250671-0-9403247960"
    }
    url = "https://smartvcode2.eastmoney.com/Titan/api/captcha/get"
    params = {
        "callback": "cb",
        "ctxid": ctxid,
        "request": req,
        "_": str(int(time.time() * 1000))
    }
    response = requests.get(url, headers=headers, cookies=cookies, params=params)
    print("请求结果:", response.text)
    if response.status_code != 200:
        print("请求失败，状态码：", response.status_code)
        return None
    return response.text
def get_ctxid():


    headers = {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,ar;q=0.7",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "Pragma": "no-cache",
        "Referer": "https://passport2.eastmoney.com/",
        "Sec-Fetch-Dest": "iframe",
        "Sec-Fetch-Mode": "navigate",
        "Sec-Fetch-Site": "same-site",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0",
        "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"Windows\""
    }
    cookies = {
        "qgqp_b_id": "0e62c6084addcd8b73f504ed7097ee3a",
        "websitepoptg_api_time": "1754106536586",
        "st_nvi": "uPvtl1P_7OmOxRkvT4AId7e10",
        "nid": "0ba7bcebb6c437b69474411fda4a96e1",
        "nid_create_time": "1754106537299",
        "gvi": "GxgX_Rp8kWc5suJsQPKMH392b",
        "gvi_create_time": "1754106537299",
        "nid_id": "295676914",
        "st_si": "71678253589540",
        "st_asi": "delete",
        "fullscreengg": "1",
        "fullscreengg2": "1",
        "RequestData": "%7b%22agentPageUrl%22%3a%22https%3a%2f%2fpassport2.eastmoney.com%2fpub%2fLoginAgent%22%2c%22redirectUrl%22%3a%22https%3a%2f%2fwww.eastmoney.com%2f%22%2c%22callBack%22%3a%22LoginCallBack%22%2c%22redirectFunc%22%3a%22PageRedirect%22%2c%22data%22%3a%7b%22domainName%22%3a%22passport2.eastmoney.com%22%2c%22deviceType%22%3a%22Web%22%2c%22productType%22%3a%22UserPassport%22%2c%22version%22%3a%220.0.1%22%7d%2c%22type%22%3anull%7d",
        "__RequestVerificationToken": "-5yJY1j_Dwhwa-HPfbeeHoUKKGZatLfYkSSxw0DUNcdJdYALChwW2a9l3dW0cMUmtvLect4l8E0-bfzw-8swfVBXwCU1",
        "p_origin": "https%3A%2F%2Fpassport2.eastmoney.com",
        "st_pvi": "59157079925959",
        "st_sp": "2025-08-02%2011%3A48%3A57",
        "st_inirUrl": "https%3A%2F%2Fpassport2.eastmoney.com%2F",
        "st_sn": "4",
        "st_psi": "20250802181413313-0-4988091101",
        "_qct": "5727d74e40da46f1bb18d9fcce4a7c56",
        "_qcu": "163.125.168.209efb6253f"
    }
    url = "https://exaccount2.eastmoney.com/home/Login4"
    params = {
        "rc": "354855057"
    }
    response = requests.get(url, headers=headers, cookies=cookies, params=params)
    html= response.text
    soup= BeautifulSoup(html, 'html.parser')
    ctxid =soup.find('input', id='hdAccountCaptContextId')
    if ctxid is None:
        print("ctxid not found")
        return
    print(response.status_code)
    print(ctxid['value'])
    return ctxid['value']
def download_image(url, path):
    resp = requests.get(url)
    if resp.status_code == 200:
        with open(path, 'wb') as f:
            f.write(resp.content)
        print(f'Saved: {path}')
        return resp.content
    else:
        raise Exception(f"Failed to download {url}. Status {resp.status_code}")
def extract_captcha_full_urls(response_text):
    match = re.search(r'cb\((\{.*\})\)', response_text)
    if not match:
        raise Exception("无效JSONP")
    data = json.loads(match.group(1))
    captcha_info = json.loads(data["Data"]["CaptchaInfo"])
    static_servers = captcha_info["static_servers"]
    protocol = "https://" if captcha_info.get("https", True) else "http://"
    base_url = static_servers[0]
    if not base_url.startswith("http"):
        base_url = protocol + base_url
    # 新增异常保护
    if not captcha_info.get("bg") or not captcha_info.get("slice") or not captcha_info.get("fullbg"):
        raise Exception(f"bg/slice/fullbg为None, 接口返回字段异常: {captcha_info}")
    bg_url = base_url.rstrip('/') + "/" + captcha_info["bg"].lstrip('/')
    slice_url = base_url.rstrip('/') + "/" + captcha_info["slice"].lstrip('/')
    fullbg_url = base_url.rstrip('/') + "/" + captcha_info["fullbg"].lstrip('/')
    return bg_url, slice_url, fullbg_url




def generate_slide_track(target_x, total_time=1200, steps=40):
    track = []
    curr_x = 0
    curr_y = 0
    elapsed = 0
    for i in range(steps // 3):
        dx = random.randint(6, 9)
        dy = random.choice([0, 0, 1, -1])
        dt = random.randint(12, 26)
        curr_x += dx
        curr_y += dy
        elapsed += dt
        track.append([curr_x, curr_y, elapsed])
    for i in range(steps // 3, steps * 2 // 3):
        dx = random.randint(3, 6)
        dy = random.choice([0, 0, 1, -1])
        dt = random.randint(18, 35)
        curr_x += dx
        curr_y += dy
        elapsed += dt
        track.append([curr_x, curr_y, elapsed])
    remain = target_x - curr_x
    remain_steps = steps - (steps * 2 // 3)
    for i in range(remain_steps):
        dx = round(remain / remain_steps)
        dy = random.choice([0, 1, -1])
        dt = random.randint(30, 50)
        curr_x += dx
        curr_y += dy
        elapsed += dt
        track.append([curr_x, curr_y, elapsed])
    if curr_x != target_x:
        elapsed += random.randint(30, 80)
        track.append([target_x, curr_y, elapsed])
    for _ in range(random.randint(2, 3)):
        elapsed += random.randint(10, 40)
        track.append([target_x + random.choice([-1,0,1]), curr_y + random.choice([-1,0,1]), elapsed])
    track_str = ':'.join('{}, {}, {}'.format(x, y, t) for x, y, t in track)
    return track_str, elapsed


if __name__ == "__main__":
    ctxid = get_ctxid()
    print("ctxid:", ctxid)
    get_bg(ctxid)  # 首次初始化
    resp2 = get_bg(ctxid)

    # ------ 1. 一次性提取出bg/slice/fullbg三个URL ------
    # 一定要先把 extract_captcha_full_urls 改成支持 fullbg，如下：
    # def extract_captcha_full_urls(response_text):
    #     ...
    #     fullbg_url = base_url.rstrip('/') + "/" + captcha_info["fullbg"].lstrip('/')
    #     return bg_url, slice_url, fullbg_url
    bg_url, slice_url, fullbg_url = extract_captcha_full_urls(resp2)
    print("bg_url:", bg_url)
    print("slice_url:", slice_url)
    print("fullbg_url:", fullbg_url)

    # ------ 2. 下载并保存三张图，包括乱序bg、滑块、完整背景 ------
    bg_bytes = download_image(bg_url, "bg.jpg")
    slice_bytes = download_image(slice_url, "slice.png")
    fullbg_bytes = download_image(fullbg_url, "fullbg.jpg")

    # ------ 3. 还原乱序背景 ------
    restored_bg = restore_bg(bg_bytes, "restored_bg.jpg")

    # ------ 4. 打印尺寸，做验证 ------
    restore_bg_img = restored_bg      # 还原后的Image对象
    fullbg_img = Image.open(io.BytesIO(fullbg_bytes))
    slice_img = Image.open(io.BytesIO(slice_bytes))
    print("restored_bg 尺寸:", restore_bg_img.size, "mode:", restore_bg_img.mode)
    print("fullbg 尺寸:", fullbg_img.size, "mode:", fullbg_img.mode)
    print("slice 尺寸:", slice_img.size, "mode:", slice_img.mode)

    # ------ 5. 用diff方法自动找缺口横坐标x ------
    # 这里会自动适配slice高度只有五六十px的新版东财验证码
    def locate_gap_by_diff(fullbg_img, restore_bg_img, gap_width=54):
        img1 = np.array(fullbg_img.convert('L'))
        img2 = np.array(restore_bg_img.convert('L'))
        _, w = img1.shape
        min_score, target_x = 1e10, 0
        for x in range(w - gap_width):
            diff = np.sum(np.abs(img1[:, x:x+gap_width] - img2[:, x:x+gap_width]))
            if diff < min_score:
                min_score, target_x = diff, x
        return target_x

    # 可用slice_img.size[0]也可以，默认取54（slice通常60x54）
    move_x = locate_gap_by_diff(fullbg_img, restore_bg_img, gap_width=slice_img.size[0])
    print(f"diff 自动定位缺口x坐标: {move_x}")

    # ------ 6. 后续滑轨生成、接口验证全部不变 ------
    d, t = generate_slide_track(move_x)
    print(f"滑轨模拟：d={d}\nt={t}")  # 打印一份调试

    validata_request(ctx, ctxid, "slide", d, t)